package com.lin.http.file;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;
import io.netty.util.internal.SystemPropertyUtil;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Pattern;

public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>{
	private static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
	private static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
	private static final int HTTP_CACHE_SECONDS = 60;

	private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
	private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-\\._]?[^<>&\\\"]*");

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
		// 先判断Http消息是否解码成功
		if (!request.decoderResult().isSuccess()) {
			sendError(ctx, HttpResponseStatus.BAD_REQUEST);
			return;
		}

		// 只允许GET方法
		if (request.method() != HttpMethod.GET) {
			sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
			return;
		}

		final String uri = request.uri();
		// uri处理为文件路径
		final String path = sanitizeUrl(uri);
		if (path == null) {
			// 路径非法，禁止访问
			sendError(ctx, HttpResponseStatus.FORBIDDEN);
			return;
		}

		File file = new File(path);
		if (file.isHidden() || !file.exists()) {
			// 文件隐藏、不存在，返回404
			sendError(ctx, HttpResponseStatus.NOT_FOUND);
			return;
		}

		if (file.isDirectory()) {
			if (uri.endsWith("/")) {
				sendList(ctx, file, uri);
			} else {
				sendRedirect(ctx, uri + "/");
			}
			return;
		}

		if (!file.isFile()) {
			sendError(ctx, HttpResponseStatus.FORBIDDEN);
			return;
		}

		// 验证缓存时间
		String ifModifiedSince = request.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE);
		if (ifModifiedSince != null && !ifModifiedSince.isEmpty()) {
			SimpleDateFormat dateFormat = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
			Date ifModifiedSinceDate = dateFormat.parse(ifModifiedSince);

			long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000;
			long lastModifiedSeconds = file.lastModified() / 1000;
			if (ifModifiedSinceDateSeconds == lastModifiedSeconds) {
				sendNotModified(ctx);
				return;
			}
		}

		RandomAccessFile raf;
		try {
			raf = new RandomAccessFile(file, "r");
		} catch (FileNotFoundException e) {
			sendError(ctx, HttpResponseStatus.NOT_FOUND);
			return;
		}

		long fileLength = raf.length();
		// 不是HttpFullRespnse,因为需要组装文件消息
		HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
		HttpUtil.setContentLength(response, fileLength);
		setContentType(response, file);
		setDateAndCacheHeaders(response, file);
		if (HttpUtil.isKeepAlive(request)) {
			response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
		}

		// 发送Http响应的第一部分，包括了Headers
		ctx.write(response);

		// 写入内容
		ChannelFuture sendFileFuture;
		ChannelFuture lastContentFuture;

		// 新建一个ChannelProgressivePromise，在写入数据传入
		// 并且ChannelProgressivePromise会作为返回值直接返回
		// ChannelProgressivePromise是一个特殊的ChannelPromise，相关的数据在传输时，它会得到通知。
		if (ctx.pipeline().get(SslHandler.class) == null) {
			// 应用程序对数据没有做任何处理，可以通过DefaultFileRegion进行发送
			// DefaultFileRegion实现了FileRegion接口，FileRegion通过Chanel来发送数据，并且支持零拷贝的文件传输。
			sendFileFuture = ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise());
			// 发送Http响应结束标记
			lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
		}else {
			// HttpChunkedInput实现ChunkedInput接口，ChunkedInput表示一种不定长的数据流，会被ChunkedWriteHandler消费。
			// HttpChunkedInput从数据块按块取出，并通过Http chunked进行传输。
			// 每个来自输入数据的块会被包装为HttpContent，在输入数据末尾，LastHttpContent是被写入。
			// 同时，确保Http响应Header设置了分块传输编码。
			sendFileFuture = ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 0, fileLength, 8192)),
					ctx.newProgressivePromise());
			// HttpChunkedInput会为我们写入结束标记（EMPTY_LAST_CONTENT）
			lastContentFuture = sendFileFuture;
		}

		// ChannelProgressiveFutureListener是一个事件监听器
		// 一旦与future相关联的发送任务被传输时，它将会被调用
		sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
			// 操作正在进行
			// 发送文件时，会被多次调用
			@Override
			public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception {
				if (total < 0) {
					System.err.println(future.channel() + " Transfer progress :" + progress);
				}else {
					System.err.println(future.channel() + " Transfer progress :" + progress + "/" + total);
				}
			}

			// 操作完成
			@Override
			public void operationComplete(ChannelProgressiveFuture future) throws Exception {
				System.err.println(future.channel() + " Transfer complete.");
			}
		});

		if (!HttpUtil.isKeepAlive(request)) {
			lastContentFuture.addListener(ChannelFutureListener.CLOSE);
		}

	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		if (ctx.channel().isActive()) {
			sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
		}
	}

	private static void setDateAndCacheHeaders(HttpResponse response, File file) {
		SimpleDateFormat dateFormat = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
		dateFormat.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));

		Calendar time = new GregorianCalendar();
		response.headers().set(HttpHeaderNames.DATE, dateFormat.format(time.getTime()));

		time.add(Calendar.SECOND, HTTP_CACHE_SECONDS);
		// 设置缓存Headers
		response.headers().set(HttpHeaderNames.EXPIRES, dateFormat.format(time.getTime()));
		response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS);
		response.headers().set(HttpHeaderNames.LAST_MODIFIED, dateFormat.format(new Date(file.lastModified())));
	}

	private static void setContentType(HttpResponse response, File file) {
		MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
		response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimetypesFileTypeMap.getContentType(file));
	}

	// 当文件的修改时间和发送过来的时间一样时，发送304
	private void sendNotModified(ChannelHandlerContext ctx) {
		FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_MODIFIED);
		setDateHeader(response);
		ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}

	// 设置Http消息的日期
	private void setDateHeader(FullHttpResponse response) {
		SimpleDateFormat dateFormat = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
		dateFormat.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));

		Calendar time = new GregorianCalendar();
		response.headers().set(HttpHeaderNames.DATE, dateFormat.format(time.getTime()));
	}

	private void sendRedirect(ChannelHandlerContext ctx, String newUri) {
		FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
		response.headers().set(HttpHeaderNames.LOCATION, newUri);
		ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}

	private void sendList(ChannelHandlerContext ctx, File dir, String dirPath) {
		FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
		response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");

		// 构建一个文件列表Html
		StringBuilder buf = new StringBuilder()
				.append("<!DOCTYPE html>\r\n")
				.append("<html><head><meta charset='utf-8' /><title>")
				.append("Listing of: ")
				.append(dirPath)
				.append("</title></head><body>\r\n")

				.append("<h3>Listing of: ")
				.append(dirPath)
				.append("</h3>\r\n")

				.append("<ul>")
				// ../ 返回上级的url,浏览器会转换url的格式
				.append("<li><a href=\"../\">..</a></li>\r\n");
		for (File file : dir.listFiles()) {
			// 跳过不可读和隐藏的文件
			if (file.isHidden() || !file.canRead()) {
				continue;
			}

			String name = file.getName();
			// 跳过名字非法的文件
			if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
				continue;
			}

			// 显示文件
			buf.append("<li><a href=\"")
					.append(name)
					.append("\">")
					.append(name)
					.append("</a></li>\r\n");
		}
		buf.append("</ul></body></html>\r\n");

		ByteBuf byteBuf = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
		response.content().writeBytes(byteBuf);
		// 释放buffer引用
		byteBuf.release();
		ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}

	private String sanitizeUrl(String uri) throws UnsupportedEncodingException {
		// 解码URI
		uri = URLDecoder.decode(uri, "UTF-8");

		if (uri.isEmpty() || uri.charAt(0) != '/') {
			return null;
		}

		// 转换统一的系统文件路径分割符
		uri = uri.replace('/', File.separatorChar);

		// 做简单判断
		if (uri.contains(File.separatorChar + ".") ||
				uri.contains("." + File.separatorChar) ||
				uri.charAt(0) == '.' || uri.charAt(uri.length() - 1) == '.' ||
				INSECURE_URI.matcher(uri).matches()) {
			return null;
		}
		// 转换绝对路径
		return SystemPropertyUtil.get("user.dir") + File.separatorChar + uri;
	}

	private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
		FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
				Unpooled.copiedBuffer("Failure:" + status + "\r\n", CharsetUtil.UTF_8));
		response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
		//	错误消息发送后尽快关闭连接
		ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}
}
